using System;
using System.Collections.Generic;
using System.ComponentModel;

namespace MvvmFx.Common.ViewModels.Behaviors.Observable
{
    /// <summary>
    /// Class that inherits from <see cref="ViewModelBase"/> and represents a 
    /// collection of <see cref="ViewModelCollection"/> objects, and provides additional 
    /// functionality for observing property changes on the collection and on the objects
    /// within it, on a per-property basis.
    /// </summary>
    /// <example>
    /// To add an event handler to changes in a named property in any of the items in the collection:
    /// <code>
    ///    this.AddItemHandler("IsSelected", OnItemSelectedChanged);
    /// </code>
    /// </example>
    /// <remarks>
    /// Adding an item handler to a ObservableViewModelCollection creates a strong object reference to it. 
    /// This means the object will not be garbage collected as long as the referencing 
    /// object has a handler to it. This can lead to memory leaks if the handlers are 
    /// not explicitly removed. This can be done in two ways:
    /// <example>
    /// Call the RemoveItemHandler method when the referencing object does not need to receive
    /// property change notifications anymore:
    /// <code>
    ///     this.RemoveItemHandler("IsSelected", OnItemSelectedChanged);
    /// </code>
    /// </example>
    /// <example>
    /// Call the Dispose method of the ObservableViewModelCollection before the object goes out of scope:
    /// <code>
    ///     documentsPage.Dispose();
    /// </code>
    /// This calls the RemoveHandlers method internally.
    /// </example>
    /// </remarks>
    /// <typeparam name="T">The type of ViewModelBase that this is a collection of.</typeparam>
    public class ObservableViewModelCollection<T> : ViewModelCollection<T>,
                                                    IDisposable
        where T : ViewModelBase
    {
        #region Fields

        /// <summary>
        /// Dictionary of property changed handlers
        /// </summary>
        /// <remarks>
        /// When a property of this object changes, e.g. Count, this dictionary
        /// is checked to see if any handlers need to be invoked.
        /// </remarks>
        private readonly Dictionary<string, Delegate> propertyChangedHandlersDictionary = new Dictionary<string, Delegate>();

        /// <summary>
        /// Dictionary of item property changed handlers
        /// </summary>
        /// <remarks>
        /// When a property of an item within this collection changes, e.g. "IsSelected", 
        /// this dictionary is checked to see if any handlers need to be invoked.
        /// </remarks>
        private readonly Dictionary<string, Delegate> itemChangedHandlersDictionary = new Dictionary<string, Delegate>();

        #endregion

        #region Events

        /// <summary>
        /// The ItemChanged event is raised whenever a property of one of the items
        /// in the collection changes value
        /// </summary>
        public event EventHandler<ItemPropertyChangedEventArgs<T>> ItemChanged;

        #endregion

        #region Protected methods

        /// <summary>
        /// Overrides the OnItemAdded method and adds and event handler
        /// to an item's PropertyChanged event when a new item is added
        /// to the collection
        /// </summary>
        /// <param name="e"></param>
        protected override void OnItemAdded(ItemEventArgs<T> e)
        {
            e.Item.PropertyChanged += this.OnItemPropertyChanged;

            base.OnItemAdded(e);
        }

        /// <summary>
        /// Overrides the OnItemRemoved method and removes the PropertChanged
        /// event handler when an item is removed from the collection
        /// </summary>
        /// <param name="e"></param>
        protected override void OnItemRemoved(ItemEventArgs<T> e)
        {
            e.Item.PropertyChanged -= this.OnItemPropertyChanged;

            base.OnItemRemoved(e);
        }

        /// <summary>
        /// Overrides the OnPropertyChanged method and notifies
        /// any handler of the property change
        /// </summary>
        /// <param name="e"></param>
        protected override void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            base.OnPropertyChanged(e);

            if (!IsSuppressingPropertyChangedNotifications)
            {
                this.NotifyPropertyChanged(e.PropertyName, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Occurs when the PropertyChanged event of one of the items
        /// in the collection is raised. Notifies any item handlers
        /// of the change.
        /// </summary>
        /// <param name="sender">The <see cref="object"/> object that fired the event.</param>
        /// <param name="e">The <see cref="PropertyChangedEventArgs"/> event fired.</param>
        protected void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (!IsSuppressingPropertyChangedNotifications)
            {
                var args = new ItemPropertyChangedEventArgs<T>(sender as T, e.PropertyName);

                if (this.ItemChanged != null)
                {
                    this.ItemChanged(this, args);
                }

                this.NotifyItemPropertyChanged(e.PropertyName, args);
            }
        }

        #endregion

        #region Property Changed Event Handlers

        /// <summary>
        /// Add a property changed event handler for the specfiied propertyName
        /// </summary>
        /// <param name="propertyName">The name of the property to attach the handler to.</param>
        /// <param name="handler">The handler to attach.</param>
        public void AddHandler(string propertyName, EventHandler handler)
        {
            Delegate handlers;
            if (this.propertyChangedHandlersDictionary.TryGetValue(propertyName, out handlers))
            {
                handlers = Delegate.Combine(handlers, handler);

                this.propertyChangedHandlersDictionary[propertyName] = handlers;
            }
            else
            {
                this.propertyChangedHandlersDictionary.Add(propertyName, handler);
            }
        }

        /// <summary>
        /// Remove a property changed event handler for the specfiied propertyName
        /// </summary>
        /// <param name="propertyName">The name of the property to remove the handlers for.</param>
        /// <param name="handler">The handler to remove.</param>
        public void RemoveHandler(string propertyName, EventHandler handler)
        {
            Delegate handlers;
            if (this.propertyChangedHandlersDictionary.TryGetValue(propertyName, out handlers))
            {
                handlers = Delegate.Remove(handlers, handler);

                if (handlers == null)
                {
                    this.propertyChangedHandlersDictionary.Remove(propertyName);
                }
                else
                {
                    this.propertyChangedHandlersDictionary[propertyName] = handlers;
                }
            }
        }

        /// <summary>
        /// Add an item property changed handler to invoke the delegate when the
        /// specified property changes on any of the items in the collection
        /// </summary>
        /// <param name="propertyName">The property name of to add the handler for.</param>
        /// <param name="handler">The handler&lt;T&gt; to add for the property name.</param>
        public void AddItemHandler(string propertyName, EventHandler<ItemPropertyChangedEventArgs<T>> handler)
        {
            Delegate handlers;
            if (this.itemChangedHandlersDictionary.TryGetValue(propertyName, out handlers))
            {
                handlers = Delegate.Combine(handlers, handler);

                this.itemChangedHandlersDictionary[propertyName] = handlers;
            }
            else
            {
                this.itemChangedHandlersDictionary.Add(propertyName, handler);
            }
        }

        /// <summary>
        /// Remove an item property changed handler.
        /// </summary>
        /// <param name="propertyName">The property name of to remove the handler for.</param>
        /// <param name="handler">The handler&lt;T&gt; to remove for the property name.</param>
        public void RemoveItemHandler(string propertyName, EventHandler<ItemPropertyChangedEventArgs<T>> handler)
        {
            Delegate handlers;
            if (this.itemChangedHandlersDictionary.TryGetValue(propertyName, out handlers))
            {
                handlers = Delegate.Remove(handlers, handler);

                if (handlers == null)
                {
                    this.itemChangedHandlersDictionary.Remove(propertyName);
                }
                else
                {
                    this.itemChangedHandlersDictionary[propertyName] = handlers;
                }
            }
        }

        /// <summary>
        /// Remove all the handlers to clean up the object references.
        /// Called by the Dispose method.
        /// </summary>
        private void RemoveHandlers()
        {
            foreach (KeyValuePair<string, Delegate> handlers in this.propertyChangedHandlersDictionary)
            {
                var delegates = handlers.Value.GetInvocationList();
                foreach (var del in delegates)
                {
                    Delegate.Remove(handlers.Value, del);
                }
            }

            foreach (KeyValuePair<string, Delegate> handlers in this.itemChangedHandlersDictionary)
            {
                var delegates = handlers.Value.GetInvocationList();
                foreach (var del in delegates)
                {
                    Delegate.Remove(handlers.Value, del);
                }
            }

            this.propertyChangedHandlersDictionary.Clear();
            this.itemChangedHandlersDictionary.Clear();
        }

        /// <summary>
        /// Notify handlers of the property that changed.
        /// </summary>
        /// <param name="propertyName">The <see cref="string"/> name of the property that changed.</param>
        /// <param name="e">The <see cref="EventArgs"/> event fired.</param>
        private void NotifyPropertyChanged(string propertyName, EventArgs e)
        {
            Delegate handler;
            if (this.propertyChangedHandlersDictionary.TryGetValue(propertyName, out handler))
            {
                /*
                 * Debug code:
                 * Delegate[] handlers = handler.GetInvocationList();
                 * int count = handlers.Length;
                 */
                handler.DynamicInvoke(this, e);
            }
        }

        /// <summary>
        /// Notify handlers of the item property that changed.
        /// </summary>
        /// <param name="propertyName">The <see cref="string"/> name of the property that changed.</param>
        /// <param name="e">The <see cref="ItemPropertyChangedEventArgs&lt;T&gt;"/> event fired.</param>
        private void NotifyItemPropertyChanged(string propertyName, ItemPropertyChangedEventArgs<T> e)
        {
            Delegate handler;
            if (this.itemChangedHandlersDictionary.TryGetValue(propertyName, out handler))
            {
                /*
                * Debug code:
                * Delegate[] handlers = handler.GetInvocationList();
                * int count = handlers.Length;
                */

                handler.DynamicInvoke(this, e);
            }
        }

        #endregion

        #region IDisposable Members

        private bool disposed;

        /// <summary>
        /// The Dispose method
        /// </summary>
        public void Dispose()
        {
            Dispose(true);

            // Use SupressFinalize in case a subclass
            // of this type implements a finalizer.
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// The protected virtual dispose that removes the handlers and clears the collection
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            // If you need thread safety, use a lock around these 
            // operations, as well as in your methods that use the resource.
            if (!disposed)
            {
                if (disposing)
                {
                    RemoveHandlers();
                    Clear();
                }

                // Indicate that the instance has been disposed.
                disposed = true;
            }
        }

        #endregion
    }
}